Multi Layer Perceptron

다층 퍼셉트론(MLP)의 역사

Minsky와 Papert는 단층의 한계를 지적했지만, 동시에 다층 퍼셉트론(Multi-Layer Perceptron, MLP)은 XOR 문제를 해결할 수 있다는 가능성을 언급했다. 은닉층(hidden layer)을 도입하면 선형적으로 분리 불가능한 문제도 해결할 수 있는 비선형 결정 경계를 만들 수 있다는 것이었다.

1980년대 중반, 여러 연구자들(특히 David Rumelhart, Geoffrey Hinton, Ronald Williams 등)이 다층 신경망을 효율적으로 훈련시킬 수 있는 역전파(Backpropagation) 알고리즘을 재발견하고 발전시켰다. 이 알고리즘은 다층 퍼셉트론의 가중치를 효과적으로 조정할 수 있게 하여, XOR 문제와 같은 비선형 문제를 성공적으로 해결할 수 있게 했다.

XOR 문제란?

XOR(Exclusive OR)는 두 입력이 서로 다를 때만 1을 출력하는 논리 연산이다. XOR 진리표는 다음과 같다:

Input 1Input 2Output
000
011
101
110

이 문제의 특징은 입력 공간에서 출력이 1인 점들 (0,1), (1,0)과 출력이 0인 점들 (0,0), (1,1)을 단일 직선으로 분리할 수 없다는 것이다. 즉, 선형적으로 분리 불가능한(non-linearly separable) 문제이다.

다층 퍼셉트론은 은닉층을 추가함으로써 비선형 변환을 수행할 수 있다. 은닉층의 뉴런들이 입력 공간을 변환하여, 원래 선형적으로 분리 불가능했던 문제를 선형적으로 분리 가능한 공간으로 매핑한다.

XOR 문제의 경우, 최소한 하나의 은닉층이 있는 2층 신경망(입력층-은닉층-출력층)으로 해결할 수 있다. 은닉층의 뉴런들이 입력을 비선형적으로 변환하여, 출력층에서는 선형 분리가 가능한 형태로 만들어준다.

이제 실제로 XOR 문제를 해결하는 다층 퍼셉트론을 구현해보자.

In [104]:
import pandas as pd
import numpy as np

# 입력 데이터 (XOR 문제)
# 각 행은 [입력1, 입력2, bias(항상 1)]를 의미
# 마지막 열의 1은 bias term (편향 항)
X = np.array([
    [0, 0, 1],
    [0, 1, 1],
    [1, 0, 1],
    [1, 1, 1]
])
y = np.array([[0], [1], [1], [0]])
np.random.seed(42)
W1 = 2 * np.random.random((4, 3)) - 1 # hidden layer
W2 = 2 * np.random.random((1, 4)) - 1 # output layer

MLP에서 활성화 함수(Activation Function)가 중요한 이유

활성화 함수는 신경망의 각 뉴런에서 입력 신호의 가중합을 받아 최종 출력을 결정하는 비선형 함수이다. 신경망의 각 층에서 가중치와 입력의 선형 결합(weighted sum)을 계산한 후, 활성화 함수를 적용하여 비선형 변환을 수행한다.

만약 활성화 함수가 없다면, 다층 신경망도 결국 선형 변환의 합성일 뿐이다. 예를 들어, 두 개의 선형 층이 있다면:

y=W2(W1x+b1)+b2=W2W1x+W2b1+b2=Wx+by = W_2(W_1 \cdot x + b_1) + b_2 = W_2 \cdot W_1 \cdot x + W_2 \cdot b_1 + b_2 = W \cdot x + b

이는 단일 선형 층과 동일하다. 따라서 활성화 함수 없이는 다층 신경망이 단일층과 동일한 표현력을 가지게 되어, XOR 문제와 같은 비선형 문제를 해결할 수 없다.

비선형 활성화 함수를 사용하면 신경망은 복잡한 비선형 함수를 근사할 수 있다. Universal Approximation Theorem에 따르면, 충분한 수의 뉴런과 적절한 활성화 함수를 가진 단일 은닉층 신경망은 임의의 연속 함수를 임의의 정확도로 근사할 수 있다.

활성화 함수는 미분이 가능해야 역전파 알고리즘을 통해 가중치를 업데이트할 수 있다. 이전에 단층 퍼셉트론 AND 게이트 학습에 사용했던 계단함수의 경우 미분이 불가능한 지점이 있으므로 다층 퍼셉트론에서는 사용할 수 없다.

활성화 함수 Sigmoid

시그모이드 함수는 가장 전통적인 활성화 함수 중 하나로, 다음과 같이 정의된다:

σ(x)=11+ex\sigma(x) = \frac{1}{1 + e^{-x}}

시그모이드 함수의 특징

  1. 출력 범위: 0과 1 사이의 값을 출력한다. 이는 확률로 해석하기 좋아 이진 분류 문제의 출력층에서 자주 사용된다.

  2. 미분 가능: 모든 구간에서 미분 가능하며, 도함수가 간단한 형태로 표현된다:
    σ(x)=σ(x)(1σ(x))\sigma'(x) = \sigma(x) \cdot (1 - \sigma(x))

  3. 부드러운 곡선: 입력값이 작을 때와 클 때 모두 부드럽게 변화하여, 학습 과정에서 안정적인 기울기를 제공한다.

시그모이드 함수는 기울기 소멸 문제(Vanishing Gradient Problem)로 인해, 깊은 신경망에서는 은닉층 활성화 함수로 잘 사용되지 않으며 ReLU 계열이 주로 사용된다. 다만 은닉층이 1개인 얕은 다층 퍼셉트론의 경우, 기울기가 연쇄적으로 곱해지는 횟수가 적기 때문에 기울기 소멸 문제가 비교적 덜 심각하며, 학습 목적이나 실습에서는 시그모이드를 사용해도 정상적으로 동작한다. 기울기 소멸에 관한 내용은 밑에서 더 자세히 다룬다.

In [105]:
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def sigmoid_derivative(x):
    return x * (1 - x)

오차 역전파 (Error Backpropagation)

역전파(Backpropagation)는 다층 퍼셉트론을 학습시키기 위한 핵심 알고리즘이다. 오차 역전파 알고리즘은 출력층에서 발생한 오차를 역방향으로 전파하여 각 층의 가중치를 업데이트한다.

최적의 가중치를 찾는다는 말은 미분을 통해 가중치의 변화에 따라 오차가 얼마나 변하는지(EW\frac{\partial E}{\partial W})를 파악해 이를 최소화할 수 있는 가중치를 만든다는 것이다.

그러나 다층 신경망에서는 오차 EE를 가중치 WW직접 미분할 수 없다. 왜냐하면 EE는 여러 층을 거쳐 계산되는 합성 함수이기 때문이다. 예를 들어, E=L(y,y^)E = L(y, \hat{y})이고 y^=σ(W2σ(W1X))\hat{y} = \sigma(W_2 \cdot \sigma(W_1 \cdot X))이므로, EEW1W_1W2W_2를 거쳐 간접적으로 의존한다. 따라서 연쇄 법칙을 사용하여 각 층을 거쳐 역방향으로 기울기를 전파해야 한다.

연쇄 법칙 (Chain Rule)이란?

합성함수의 미분은 각 함수를 구성하는 함수의 미분의 곱으로 나타낼 수 있다. 신경망에서 손실 함수 LL이 여러 층을 거쳐 계산되므로, 각 가중치에 대한 기울기를 구하기 위해서는 연쇄 법칙이 필요하다.

역전파에서 연쇄 법칙의 핵심 아이디어는 다음과 같다:

신호 E(오차)에 노드의 국소적 미분(로컬 그래디언트)을 곱한 후 → 다음 노드에 전달하는 것

EW1=Ey^y^a1a1z1z1W1\frac{\partial E}{\partial W_1} = \frac{\partial E}{\partial \hat{y}} \cdot \frac{\partial \hat{y}}{\partial a_1} \cdot \frac{\partial a_1}{\partial z_1} \cdot \frac{\partial z_1}{\partial W_1}

연쇄 법칙의 동작 과정:

  1. 출력층: 오차 신호 E=yy^E = y - \hat{y}에 출력층의 국소적 미분 σ(y^)\sigma'(\hat{y})를 곱하여 δout\delta_{out}을 계산
  2. 은닉층: δout\delta_{out}에 가중치 W2W_2를 곱한 후, 은닉층의 국소적 미분 σ(a1)\sigma'(a_1)을 곱하여 δhidden\delta_{hidden}을 계산
  3. 입력층: δhidden\delta_{hidden}을 사용하여 입력층의 가중치 W1W_1에 대한 기울기를 계산

이렇게 각 노드에서 국소적 미분을 계산하고, 이전 층에서 전달된 오차 신호와 곱하여 다음 층으로 전파하는 것이 연쇄 법칙의 핵심이다.

역전파 알고리즘의 동작 원리

역전파 알고리즘은 다음과 같은 단계로 구성된다:

  1. 순전파(Forward Propagation): 입력 데이터를 신경망을 통해 전달하여 예측값을 계산한다.
  2. 손실 계산(Loss Calculation): 예측값과 실제값의 차이를 계산한다.
  3. 역전파(Backward Propagation): 출력층에서 시작하여 각 층의 오차 신호(델타)를 계산한다.
  4. 가중치 업데이트(Weight Update): 계산된 오차 신호를 사용하여 경사 하강법으로 가중치를 업데이트한다.

역전파 알고리즘은 연쇄 법칙 (Chain Rule)을 사용하여 각 가중치에 대한 손실 함수의 기울기를 계산한다.

출력층의 오차 신호

출력층의 오차 신호는 다음과 같이 계산된다:

δout=(yy^)σ(y^)δ_{out} = (y - \hat{y}) \cdot \sigma'(\hat{y})

여기서:

  • yy^y - \hat{y}: 예측 오차
  • σ(y^)=y^(1y^)\sigma'(\hat{y}) = \hat{y}(1 - \hat{y}): 시그모이드 함수의 도함수

은닉층의 오차 신호

은닉층의 오차 신호는 출력층의 오차 신호를 역방향으로 전파하여 계산한다:

δhidden=(δoutW2)σ(a1)δ_{hidden} = (δ_{out} \cdot W_2) \cdot \sigma'(a_1)

여기서:

  • δoutW2δ_{out} \cdot W_2: 출력층 오차를 은닉층으로 전파
  • σ(a1)=a1(1a1)\sigma'(a_1) = a_1(1 - a_1): 은닉층 활성화 함수의 도함수

가중치 업데이트

각 가중치 행렬은 다음과 같이 업데이트된다:

W2=W2+αδoutTa1W_2 = W_2 + \alpha \cdot δ_{out}^T \cdot a_1

W1=W1+αδhiddenTXW_1 = W_1 + \alpha \cdot δ_{hidden}^T \cdot X

여기서 α\alpha는 학습률(learning rate)이다.

알고리즘의 장점

  1. 효율성: 모든 가중치에 대한 기울기를 한 번의 순전파와 역전파로 계산할 수 있다.
  2. 일반화: 여러 층으로 확장 가능하며, 다양한 활성화 함수와 손실 함수에 적용할 수 있다.
  3. 자동 미분: 연쇄 법칙을 통해 복잡한 미분 계산을 자동으로 수행한다.

구현 코드 설명

아래 코드는 3×4×1 구조의 다층 퍼셉트론을 역전파 알고리즘으로 학습하는 과정을 보여준다:

  • 순전파: 입력 X를 은닉층을 거쳐 출력층까지 전달
  • 손실 계산: 평균 제곱 오차(MSE)를 사용
  • 역전파: 출력층에서 은닉층으로 오차 신호 전파
  • 가중치 업데이트: 경사 하강법으로 가중치 조정
In [106]:
alpha = 0.9
epochs = 10000
# 가중치와 오차의 변화를 기록
hist_w = []
hist_loss = []

for epoch in range(epochs):
    # ===== 순전파 =====
    z1 = np.dot(X, W1.T)          # (4, 4)
    a1 = sigmoid(z1)              # hidden activation

    z2 = np.dot(a1, W2.T)         # (4, 1)
    y_hat = sigmoid(z2)           # output

    # ===== 손실 계산 =====
    error = y - y_hat
    loss = np.mean(error ** 2)
    hist_loss.append(loss)

    # ===== 역전파 =====
    d_out = error * sigmoid_derivative(y_hat)      # (4, 1)
    d_hidden = np.dot(d_out, W2) * sigmoid_derivative(a1)  # (4, 4)

    # ===== 가중치 업데이트 =====
    W2 += alpha * np.dot(d_out.T, a1)
    W1 += alpha * np.dot(d_hidden.T, X)

    hist_w.append({
        "W1": W1.copy(),
        "W2": W2.copy()
    })
    if epoch % 2000 == 0:
        print(f"Epoch {epoch}, Loss: {loss:.6f}")

# ===== 최종 결과 =====
print("\nFinal prediction:")
print(np.round(y_hat, 3))
Epoch 0, Loss: 0.263420
Epoch 2000, Loss: 0.000977
Epoch 4000, Loss: 0.000357
Epoch 6000, Loss: 0.000207
Epoch 8000, Loss: 0.000142

Final prediction:
[[0.005]
 [0.989]
 [0.989]
 [0.013]]
In [107]:
import matplotlib.pyplot as plt

# 학습 과정에서 오차(delta) 변화 시각화
# epoch가 증가할수록 오차가 감소하는지 확인
plt.plot(hist_loss)
plt.xlabel("epoch")
plt.ylabel("delta (error)")
plt.title("Error Change During Training")
plt.show()
Notebook output
In [108]:
# 은닉층 0번 뉴런의 가중치 추적
w_input1 = [w["W1"][0, 0] for w in hist_w]
w_input2 = [w["W1"][0, 1] for w in hist_w]
w_bias   = [w["W1"][0, 2] for w in hist_w]

plt.plot(w_input1, label="input1")
plt.plot(w_input2, label="input2")
plt.plot(w_bias, label="bias")

plt.xlabel("Epoch")
plt.ylabel("Weight value")
plt.title("Hidden Neuron 0 Weight Changes")
plt.legend()
plt.grid(True)
plt.show()
Notebook output
In [111]:
# ===== grid 생성 =====
xx, yy = np.meshgrid(
    np.linspace(-0.1, 1.5, 300),
    np.linspace(-0.1, 1.5, 300)
)

grid = np.c_[xx.ravel(), yy.ravel(), yy.ravel() * 0 + 1]  # bias = 1

# ===== 모델 예측 =====
z1 = np.dot(grid, W1.T)
a1 = sigmoid(z1)
z2 = np.dot(a1, W2.T)
y_pred = sigmoid(z2)

Z = (y_pred > 0.5).reshape(xx.shape)

# ===== figure =====
plt.figure(figsize=(6, 6))

# 데이터 포인트
plt.scatter(X[y.flatten() == 0, 0], X[y.flatten() == 0, 1],
            c='red', s=150, marker='o', label='Output = 0',
            edgecolors='black', linewidths=2, alpha=0.7)

plt.scatter(X[y.flatten() == 1, 0], X[y.flatten() == 1, 1],
            c='blue', s=150, marker='s', label='Output = 1',
            edgecolors='black', linewidths=2, alpha=0.7)

# ===== decision boundary (곡선) =====
plt.contour(xx, yy, Z, levels=[0.5],
            colors='green', linewidths=3, alpha=0.8)

# 축선 (AND 코드와 동일)
plt.axhline(y=0, color='black', linewidth=1, alpha=0.5)
plt.axvline(x=0, color='black', linewidth=1, alpha=0.5)

plt.xlim(-0.1, 1.5)
plt.ylim(-0.1, 1.5)
plt.xlabel('x1')
plt.ylabel('x2')
plt.title('XOR Decision Boundary')
plt.grid(True, alpha=0.3, linestyle='--')
plt.legend(fontsize=12, loc='upper right')
plt.show()
Notebook output

핵심 개념 정리

  • 다층 퍼셉트론(MLP): 은닉층을 가진 신경망으로, 단층 퍼셉트론이 해결할 수 없는 XOR 같은 비선형 문제를 해결할 수 있다.

  • XOR 문제: 단일 직선으로 분리할 수 없는 선형 분리 불가능한 문제로, 다층 퍼셉트론의 필요성을 보여주는 대표적인 예시이다.

  • 활성화 함수: 신경망에 비선형성을 도입하여 복잡한 함수를 근사할 수 있게 하며, 역전파를 위해 미분 가능해야 한다.

  • 시그모이드 함수: 0과 1 사이의 값을 출력하는 전통적인 활성화 함수로, 도함수가 σ(x)=σ(x)(1σ(x))\sigma'(x) = \sigma(x)(1-\sigma(x))로 간단하게 표현된다.

  • 역전파(Backpropagation): 출력층에서 발생한 오차를 역방향으로 전파하여 각 층의 가중치에 대한 기울기(EW\frac{\partial E}{\partial W})를 계산하는 알고리즘이다.

  • 연쇄 법칙(Chain Rule): 합성 함수의 미분을 각 함수의 미분의 곱으로 계산하는 방법으로, 역전파에서 오차 신호에 국소적 미분을 곱해 다음 층으로 전파하는 핵심 원리이다.

  • 순전파(Forward Propagation): 입력 데이터를 신경망을 통해 전달하여 예측값을 계산하는 과정이다.

  • 기울기 소멸 문제: 깊은 신경망에서 시그모이드 함수의 도함수가 1보다 작아 역전파 과정에서 기울기가 0에 가까워지는 현상으로, ReLU 같은 활성화 함수로 완화할 수 있다.